package br.com.ppj.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.StringTokenizer;

public class IoTools {
  
  public static final int
         DEFAULT_IO_WAIT         = 200,
         MIN_READ_BLOCK_SIZE     = 1024,
         MAX_READ_BLOCK_SIZE     = 64 * 1024,
         DEFAULT_READ_BLOCK_SIZE = 4 * 1024;

  private IoTools() { }

  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // Abstract file operations
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  
  public static String assertFile(String path, boolean read, boolean write) {
    File file = IoTools.createFile(path);
    if (file == null) {
      throw new IllegalArgumentException("Unable to create directory '" + path + "'");
    }
    return assertFile(file, read, write);
  }

  public static String assertFile(File file, boolean read, boolean write) {
    String result = file.getAbsolutePath();
    if (!file.exists() && !file.mkdirs()) {
      throw new IllegalArgumentException("Unable to create the directory chain for '" + result + "'");
    }
    if (file.isDirectory()) {
      throw new IllegalArgumentException("'" + result + "' exists but is a directory");
    }
    if (read && !file.canRead()) {
      throw new IllegalArgumentException("Cannot read from '" + result + "'");
    }
    if (write && !file.canWrite()) {
      throw new IllegalArgumentException("Cannot write to '" + result + "'");
    }
    return result;
  }

  public static File createFile(String path) {
    return createFile(path, IoTools.class);
  }

  public static File createFile(String path, Class<?> refClass) {
    File result;
    URI uri = null;
    String scheme = null;
    try {
      uri = new URI(path);
      scheme = uri.getScheme();
    } catch (Exception _) { }
    if (uri == null || scheme == null || !scheme.equals("file") ||
        !uri.isAbsolute() || uri.isOpaque() ||
        uri.getAuthority() != null || uri.getFragment() != null ||
        uri.getQuery() != null || uri.getPath().length() == 0) {
      if (path.startsWith("file:")) {
        path = _stripPath(path);
      } else {
        if (refClass == null) {
          throw new IllegalArgumentException("Reference class must be specified");
        }
        URL url = refClass.getResource(path);
        if (url != null) {
          path = url.toExternalForm();
          if (path.startsWith("file:")) {
            path = _stripPath(path);
          }
        }
      }
      result = new File(path);
    } else {
      result = new File(uri);
    }
    return result;
  }
  
  private static String _stripPath(String path) {
    // (A) file:/...     -> /...
    // (B) file://...    -> /...
    // (C) file:/X:/...  -> X:/...
    // (D) file://X:/... -> X:/...
    // (E) file:...      -> ...      (possibly, malformed)
    String result;
    if (path.charAt(5) == '/') {
      if (path.charAt(6) == '/' && path.charAt(8) == ':') {
        result = path.substring(7);                                  // (D)
      } else if (path.charAt(6) == '/' || path.charAt(7) == ':') {
        result = path.substring(6);                                  // (B)(C)
      } else {
        result = path.substring(5);                                  // (A)
      }
    } else {
      result = path.substring(5);                                    // (E)
    }
    return result;
  }
  
  public static File createFile(String path, byte[] data) throws IOException {
    return createFile(path, data, IoTools.class);
  }
  
  public static File createFile(String path, byte[] data, Class<?> refClass) throws IOException {
    File result = createFile(path, refClass);
    writeBytes(result, data, 0, data.length, (short)0);
    return result;
  }
  
  public static boolean removeFile(String path) {
    return removeFile(path, IoTools.class);
  }  

  public static boolean moveFile(File file, File dir ) throws SecurityException  {
    File dest = new File(dir, file.getName());	
	if (dest.exists()) {
		  boolean remove = IoTools.removeFile(dest);
		}		
	boolean result = file.renameTo(dest);
    return result;
  }
  
  public static boolean removeFile(String path, Class<?> refClass) throws SecurityException {
    boolean result;
    File file = createFile(path, refClass);
    synchronized (String.valueOf(file.getAbsolutePath().hashCode()).intern()) {
      result = file.delete();
    }
    return result;
  }

  public static boolean removeFile(File file) throws SecurityException {
    boolean result;
    synchronized (String.valueOf(file.getAbsolutePath().hashCode()).intern()) {
      result = file.delete();
    }
    return result;
  }
  
  public static String toFilename(String name) {
    return toFilename(name, '\0');
  }
  
  public static String toFilename(String key, char scopeSeparator) {
    char[] chars = key.toCharArray();
    char[] fileChars = new char[chars.length];
    char chr;
    for (int i = 0; i < chars.length; i++) {
      chr = chars[i];
      if (scopeSeparator > '\0' && chr == scopeSeparator) {
        fileChars[i] = '/';
      } else {
        switch (chr) {
          case '.':
          case '/':
          case '\\':
          case ' ':
          case ':':
          case ';':
          case '"':
          case '\'':
            fileChars[i] = '_';
            break;
          default:
            fileChars[i] = chr;
        }
      }
    }
    return new String(fileChars);
  }
  
  public static String getNameFromPath(String path) {
    if (path.indexOf('\\') >= 0) {
      path = path.replace('\\', '/');
    }
    int pos = path.lastIndexOf('/');
    if (pos >= 0) {
      path = path.substring(pos + 1);
    }
    return path;
  }
  
  public static String getNameProperFromPath(String path) {
    String result = getNameFromPath(path);
    int pos = result.lastIndexOf('.');
    if (pos >= 0) {
      result = result.substring(0, pos);
    }
    return result;
  }
  
  public static String getExtensionFromPath(String path) {
    String result = getNameFromPath(path);
    int pos = result.lastIndexOf('.');
    if (pos >= 0) {
      result = result.substring(pos + 1);
    }
    return result;
  }

  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // Directory operations
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////

  public static String assertDir(String path, boolean read, boolean write) {
    File file = IoTools.createFile(path);
    if (file == null) {
      throw new IllegalArgumentException("Unable to create directory '" + path + "'");
    }
    return assertDir(file, read, write);
  }

  public static String assertDir(File file, boolean read, boolean write) {
    String result = file.getAbsolutePath();
    if (!file.exists() && !file.mkdirs()) {
      throw new IllegalArgumentException("Unable to create the directory chain for '" + result + "'");
    }
    if (!file.isDirectory()) {
      throw new IllegalArgumentException("'" + result + "' exists and is not a directory");
    }
    if (read && !file.canRead()) {
      throw new IllegalArgumentException("Cannot read from '" + result + "'");
    }
    if (write && !file.canWrite()) {
      throw new IllegalArgumentException("Cannot write to '" + result + "'");
    }
    return result;
  }

  public static String getTemporaryDir() {
    String result = System.getProperty("java.io.tmpdir");
    if (result != null && result.length() != 0) {
      result = "file://" + result.replace('\\', '/');
      if (result.endsWith("/")) {
        result = result.substring(0, result.length() - 1);
      }
    }
    return result;
  }

  public static boolean clearDir(String dir) {
    File file = createFile(dir);
    return clearDir(file);
  }

  public static boolean clearDir(File dir) throws SecurityException {
    boolean result = true;
    synchronized (String.valueOf(dir.getAbsolutePath().hashCode()).intern()) {
      if (dir.exists()) {
        File file;
        File[] files = dir.listFiles();
        for (int i = 0; i < files.length; i++) {
          file = files[i];
          if (file.isDirectory()) {
            result = removeDir(file);
          } else {
            synchronized (String.valueOf(file.getAbsolutePath().hashCode()).intern()) {
              result = file.delete();
            }
          }
          if (!result) {
            break;
          }
        }
      }
    }
    return result;
  }

  public static boolean removeDir(String dir) throws SecurityException {
    return removeDir(createFile(dir));
  }

  public static boolean removeDir(File dir) throws SecurityException {
    boolean result = true;
    synchronized (String.valueOf(dir.getAbsolutePath().hashCode()).intern()) {
      if (dir.exists()) {
        result = clearDir(dir);
        if (result) {
          result = dir.delete();
        }
      }
    }
    return result;
  }

  public static boolean removeFiles(String dir) throws SecurityException {
    return removeFiles(createFile(dir));
  }

  public static boolean removeFiles(File dir) throws SecurityException {
    boolean result = true;
    synchronized (String.valueOf(dir.getAbsolutePath().hashCode()).intern()) {
      if (dir.exists()) {
        File file;
        File[] files = dir.listFiles();
        for (int i = 0; i < files.length; i++) {
          file = files[i];
          if (!file.isDirectory()) {
            synchronized (String.valueOf(file.getAbsolutePath().hashCode()).intern()) {
              result = file.delete();
            }
            if (!result) {
              break;
            }
          }
        }
      }
    }
    return result;
  }

  public static String readFileAsString(File file) throws java.io.IOException {
	  return readFileAsString(file, "UTF-8");
  }

  public static String readFileAsString(File file, String charset) throws java.io.IOException {
		long length = file.length();
		byte[] buffer = new byte[(int)length];
	    BufferedInputStream f = null;
	    try {
	        FileInputStream fis = new FileInputStream(file);
			f = new BufferedInputStream(fis);
	        f.read(buffer);
	    } finally {
	        if (f != null) {
	        	try { 
	        		f.close(); 
	        	} catch (IOException ignored) { }
	        }
	    }
	    String result = new String(buffer, charset);
//	    String result = new String(buffer, "UTF-8");
		return result;
	}

  
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // Properties operations
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  
  public static Properties propertiesFromFile(String properName, Locale locale) {
    return propertiesFromFile(properName, (String)null, locale, (String)null, (String)null);
  }
  
  public static Properties propertiesFromFile(String properName,
                                              Locale locale, String currPrefix) {
    return propertiesFromFile(properName, (String)null, locale, currPrefix, (String)null);
  }
  
  public static Properties propertiesFromFile(String properName,
                                              Locale locale, String currPrefix, String newPrefix) {
    return propertiesFromFile(properName, (String)null, locale, currPrefix, newPrefix);
  }
  
  public static Properties propertiesFromFile(String properName, String customName, Locale locale) {
    return propertiesFromFile(properName, customName, locale, (String)null, (String)null);
  }
  
  public static Properties propertiesFromFile(String properName, String customName,
                                              Locale locale, String currPrefix) {
    return propertiesFromFile(properName, customName, locale, currPrefix, (String)null);
  }
  
  public static Properties propertiesFromFile(String properName, String customName,
                                              Locale locale, String currPrefix, String newPrefix) {
    Properties result = new Properties();
    if (currPrefix != null && currPrefix.length() > 0) {
      currPrefix += ".";
    }
    if (newPrefix == null) {
      newPrefix = "";
    } else if (newPrefix.length() > 0) {
      newPrefix += ".";
    }
    if (locale == null) {
      locale = new Locale("pt", "BR");
    }
    ResourceBundle properBundle = ResourceBundle.getBundle(properName, locale);
    ResourceBundle customBundle = (customName == null ? null : ResourceBundle.getBundle(customName, locale));
    String key, value;
    Enumeration<String> en = properBundle.getKeys();
    while (en.hasMoreElements()) {
      key = en.nextElement();
      value = (customBundle == null ?
               properBundle.getString(key) : getString(properBundle, customBundle, key));
      if (currPrefix == null) {
        result.put(newPrefix + key, value);
      } else if (key.startsWith(currPrefix)) {
        result.put(newPrefix + key.substring(currPrefix.length()), value);
      }
    }
    return result;
  }
  
  public static boolean getBoolean(ResourceBundle properBundle, ResourceBundle customBundle, String key) {
    String str;
    try {
      str = customBundle.getString(key);
    } catch (MissingResourceException e) {
      str = properBundle.getString(key);
    }
    boolean result = Boolean.parseBoolean(str);
    return result;
  }
  
  public static boolean getBoolean(ResourceBundle properBundle, ResourceBundle customBundle,
                                   String key, boolean defValue) {
    boolean result;
    String str;
    try {
      str = customBundle.getString(key);
      result = Boolean.parseBoolean(str);
    } catch (MissingResourceException e) {
      try {
        str = properBundle.getString(key);
        result = Boolean.parseBoolean(str);
      } catch (MissingResourceException ex) {
        result = defValue;
      }
    }
    return result;
  }
  
  public static double getDouble(ResourceBundle properBundle, ResourceBundle customBundle, String key) {
    String str;
    try {
      str = customBundle.getString(key);
    } catch (MissingResourceException e) {
      str = properBundle.getString(key);
    }
    double result = Double.parseDouble(str);
    return result;
  }
  
  public static double getDouble(ResourceBundle properBundle, ResourceBundle customBundle,
                                 String key, double defValue) {
    double result;
    String str;
    try {
      str = customBundle.getString(key);
      result = Double.parseDouble(str);
    } catch (MissingResourceException e) {
      try {
        str = properBundle.getString(key);
        result = Double.parseDouble(str);
      } catch (MissingResourceException ex) {
        result = defValue;
      }
    }
    return result;
  }
  
  public static int getInt(ResourceBundle properBundle, ResourceBundle customBundle, String key) {
    String str;
    try {
      str = customBundle.getString(key);
    } catch (MissingResourceException e) {
      str = properBundle.getString(key);
    }
    int result = Integer.parseInt(str);
    return result;
  }
  
  public static int getInt(ResourceBundle properBundle, ResourceBundle customBundle,
                           String key, int defValue) {
    int result;
    String str;
    try {
      str = customBundle.getString(key);
      result = Integer.parseInt(str);
    } catch (MissingResourceException e) {
      try {
        str = properBundle.getString(key);
        result = Integer.parseInt(str);
      } catch (MissingResourceException ex) {
        result = defValue;
      }
    }
    return result;
  }
  
  public static String getString(ResourceBundle properBundle, ResourceBundle customBundle, String key) {
    String result;
    try {
      result = customBundle.getString(key);
    } catch (MissingResourceException e) {
      result = properBundle.getString(key);
    }
    return result;
  }
  
  public static String getString(ResourceBundle properBundle, ResourceBundle customBundle,
                                 String key, String defValue) {
    String result;
    try {
      result = customBundle.getString(key);
    } catch (MissingResourceException e) {
      try {
        result = properBundle.getString(key);
      } catch (MissingResourceException ex) {
        result = defValue;
      }
    }
    return result;
  }
  
  public static String[] getStrings(ResourceBundle properBundle, ResourceBundle customBundle, String key) {
    String str;
    try {
      str = customBundle.getString(key);
    } catch (MissingResourceException e) {
      str = properBundle.getString(key);
    }
    return _toStringArray(str);
  }
  
  public static String[] getStrings(ResourceBundle properBundle, ResourceBundle customBundle,
                                    String key, String[] defValue) {
    String[] result;
    String str;
    try {
      str = customBundle.getString(key);
      result = _toStringArray(str);
    } catch (MissingResourceException e) {
      try {
        str = properBundle.getString(key);
        result = _toStringArray(str);
      } catch (MissingResourceException ex) {
        result = defValue;
      }
    }
    return result;
  }
  
  private static String[] _toStringArray(String str) {
    List<String> list = null;
    if (str != null && str.length() > 0) {
      list = new ArrayList<String>();
      StringTokenizer tokenizer = new StringTokenizer(str, ", ");
      while (tokenizer.hasMoreTokens()) {
        list.add(tokenizer.nextToken());
      }
    }
    final int size = (list == null ? 0 : list.size());
    String[] result = (size == 0 ? null :  list.toArray(new String[size]));
    return result;
  }

  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // Marshalling/unmarshalling operations
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  
  public static byte[] marshall(Object obj) throws IOException {
    return marshall(obj, (byte[])null);
  }
  
  public static byte[] marshall(Object obj, byte[] leader) throws IOException {
    byte[] result;
    // no need to close a ByteArrayOutputStream
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    ObjectOutputStream oout = null;
    try {
      if (leader != null) {
        // must write leader with 'out', before creating 'oout'
        out.write(leader);
      }
      oout = new ObjectOutputStream(out);
      oout.writeObject(obj);
      oout.flush();
      result = out.toByteArray();
    } finally {
      if (oout != null) {
        try {
          oout.close();
        } catch (Exception _) { }
      }
    }
    return result;
  }
  
  public static void marshall(byte[] target, Object obj) throws IOException {
    marshall(target, 0, obj);
  }
  
  public static void marshall(byte[] target, int offset, Object obj) throws IOException {
    // no need to close a ByteArrayOutputStream
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    ObjectOutputStream oout = new ObjectOutputStream(out);
    try {
      oout.writeObject(obj);
      oout.flush();
      byte[] temp = out.toByteArray();
      System.arraycopy(temp, 0, target, offset, temp.length);
    } finally {
      try {
        oout.close();
      } catch (Exception _) { }
    }
  }
  
  public static void marshall(File target, Object obj) throws IOException {
    _marshall(target, obj, null, true, (short)0);
  }
  
  public static void marshall(File target, Object obj, boolean append) throws IOException {
    _marshall(target, obj, null, append, (short)0);
  }
  
  public static void marshall(File target, Object obj, short retries) throws IOException {
    _marshall(target, obj, null, true, retries);
  }
  
  public static void marshall(File target, Object obj, boolean append, short retries) throws IOException {
    _marshall(target, obj, null, append, retries);
  }
  
  public static void marshall(File target, Object obj, byte[] leader) throws IOException {
    _marshall(target, obj, leader, false, (short)0);
  }
  
  public static void marshall(File target, Object obj, byte[] leader, short retries) throws IOException {
    _marshall(target, obj, leader, false, retries);
  }

  // pre-condition:
  //   !append || leader == null
  private static void _marshall(File target, Object obj, byte[] leader, boolean append, short retries)
                     throws IOException {
    FileOutputStream fout = null;
    try {
      fout = new FileOutputStream(target, append);
      if (leader != null) {
        // must write leader with 'fout', before creating oout
        fout.write(leader);
      }
      // no need to close 'out' or 'oout',
      // as the inner-most 'fout' will be closed
      OutputStream out = new BufferedOutputStream(fout);
      ObjectOutputStream oout = new ObjectOutputStream(out);
      oout.writeObject(obj);
      oout.flush();
    } catch (FileNotFoundException exc) {
      if (retries <= 0) {
        throw exc;
      }
      _hold(DEFAULT_IO_WAIT);
      _marshall(target, obj, leader, append, --retries); 
    } finally {
      if (fout != null) {
        try {
          fout.close();
        } catch (Exception _) { }
      }
    }
  }
  
  public static void marshall(OutputStream target, Object obj) throws IOException {
    marshall(target, obj, null);
  }
  
  public static void marshall(OutputStream target, Object obj, byte[] leader) throws IOException {
    if (leader != null) {
      if (target instanceof ObjectOutputStream) {
        throw new IllegalStateException("Cannot write leader bytes directly into an ObjectOutputStream");
      }
      // must write leader with 'target', before creating 'oout'
      target.write(leader);
    }
    ObjectOutputStream oout = (target instanceof ObjectOutputStream ?
                               (ObjectOutputStream)target : new ObjectOutputStream(target));
    oout.writeObject(obj);
    oout.flush();    
  }
  
  public static Object unmarshall(byte[] source) throws ClassNotFoundException, IOException {
    return unmarshall(source, 0, source.length);
  }
  
  public static Object unmarshall(byte[] source, int offset, int size)
                       throws ClassNotFoundException, IOException {
    Object result;
    // no need to close a ByteArrayInputStream
    ByteArrayInputStream in = new ByteArrayInputStream(source, offset, size);
    ObjectInputStream oin = new ObjectInputStream(in);
    try {
      result = oin.readObject();
    } finally {
      try {
        oin.close();
      } catch (Exception _) { }
    }
    return result;
  }
  
  public static Object unmarshall(File source) throws ClassNotFoundException, IOException {
    return _unmarshall(source, 0L, null, (short)0);
  }
  
  public static Object unmarshall(File source, long position) throws ClassNotFoundException, IOException {
    return _unmarshall(source, position, null, (short)0);
  }
  
  public static Object unmarshall(File source, long position, short retries)
                       throws ClassNotFoundException, IOException {
    return _unmarshall(source, position, null, retries);
  }
  
  public static Object unmarshall(File source, byte[] leader) throws ClassNotFoundException, IOException {
    return _unmarshall(source, 0L, leader, (short)0);
  }
  
  public static Object unmarshall(File source, byte[] leader, short retries)
                       throws ClassNotFoundException, IOException {
    return _unmarshall(source, 0L, leader, retries);
  }
  
  // pre-condition:
  //   position == 0 || leader == null
  private static Object _unmarshall(File source, long position, byte[] leader, short retries)
                        throws ClassNotFoundException, IOException {
    Object result = null;
    if (source.exists()) {
      FileInputStream fin = null;
      try {
        fin = new FileInputStream(source);
        if (position > 0) {
          // must skip bytes with 'fin', before creating 'oin'
          skipBytes(fin, position);
        } else if (leader != null && leader.length > 0) {
          // must read leader with 'fin', before creating 'oin'
          fin.read(leader);
        }
        // no need to close 'oin', as the inner-most 'fin' will be closed
        ObjectInputStream oin = new ObjectInputStream(fin);
        result = oin.readObject();
      } catch (FileNotFoundException exc) {
        if (retries <= 0) {
          throw exc;
        }
        _hold(DEFAULT_IO_WAIT);
        result = _unmarshall(source, position, leader, --retries);
      } finally {
        if (fin != null) {
          try {
            fin.close();
          } catch (Exception _) { }
        }
      }
    }
    return result;
  }
  
  public static Object unmarshall(InputStream source) throws ClassNotFoundException, IOException {
    return _unmarshall(source, 0, null);
  }
  
  public static Object unmarshall(InputStream source, int offset) throws ClassNotFoundException, IOException {
    return _unmarshall(source, offset, null);
  }
  
  public static Object unmarshall(InputStream source, byte[] leader)
                       throws ClassNotFoundException, IOException {
    return _unmarshall(source, 0, leader);
  }

  // pre-condition:
  //   offset == 0 || leader == null
  private static Object _unmarshall(InputStream source, int offset, byte[] leader)
                        throws ClassNotFoundException, IOException {
    if (offset > 0) {
      // must skip bytes with 'source', before creating 'oin'
      skipBytes(source, offset);
    } else if (leader != null && leader.length > 0) {
      // must read leader with 'source', before creating 'oin'
      source.read(leader);
    }
    ObjectInputStream oin = new ObjectInputStream(source);
    return oin.readObject();
  }

  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // Sequential file operations
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  
  public static byte[] readBytes(File source) throws IOException {
    int maxReadSize = (int)source.length();
    int blockSize = _getOptimalBlockSize(maxReadSize);
    return readBytes(source, 0L, maxReadSize, blockSize, (short)0);
  }
  
  public static byte[] readBytes(File source, int maxReadSize) throws IOException {
    int blockSize = _getOptimalBlockSize(maxReadSize);
    return readBytes(source, 0L, maxReadSize, blockSize, (short)0);
  }
  
  public static byte[] readBytes(File source, int maxReadSize, int blockSize) throws IOException {
    return readBytes(source, 0L, maxReadSize, blockSize, (short)0);
  }
  
  public static byte[] readBytes(File source, short retries) throws IOException {
    int maxReadSize = (int)source.length();
    int blockSize = _getOptimalBlockSize(maxReadSize);
    return readBytes(source, 0L, maxReadSize, blockSize, retries);
  }
  
  public static byte[] readBytes(File source, int maxReadSize, short retries) throws IOException {
    int blockSize = _getOptimalBlockSize(maxReadSize);
    return readBytes(source, 0L, maxReadSize, blockSize, retries);
  }

  public static byte[] readBytes(File source, long position, int maxReadSize) throws IOException {
    int blockSize = _getOptimalBlockSize(maxReadSize);
    return readBytes(source, position, maxReadSize, blockSize, (short)0);
  }
  
  public static byte[] readBytes(File source, long position, int maxReadSize, short retries)
                       throws IOException {
    return readBytes(source, position, maxReadSize, 0, retries);
  }
  
  public static byte[] readBytes(File source, long position, int maxReadSize, int blockSize, short retries)
                       throws IOException {
    byte[] result = null;
    if (source.exists()) {
      InputStream in = null;
      try {
        // no need to close 'fin', as the outer 'in' will be closed
        FileInputStream fin = new FileInputStream(source);
        in = new BufferedInputStream(fin, blockSize);
        if (position > 0) {
          skipBytes(in, position);
        }
        result = readBytes(in, maxReadSize, blockSize);
      } catch (FileNotFoundException exc) {
        if (retries <= 0) {
          throw exc;
        }
        _hold(DEFAULT_IO_WAIT);
        result = readBytes(source, position, maxReadSize, blockSize, --retries); 
      } finally {
        if (in != null) {
          try {
            in.close();
          } catch (Exception _) { }
        }
      }
    }
    return result;
  }

  public static void writeBytes(File target, byte[] data) throws IOException {
    writeBytes(target, data, 0, data.length, (short)0);
  }

  public static void writeBytes(File target, byte[] data, short retries) throws IOException {
    writeBytes(target, data, 0, data.length, retries);
  }

  public static void writeBytes(File target, byte[] data, int offset, int size) throws IOException {
    writeBytes(target, data, offset, size, (short)0);
  }

  public static void writeBytes(File target, byte[] data, int offset, int size, short retries)
                     throws IOException {
    OutputStream out = null;
    try {
      FileOutputStream fout = new FileOutputStream(target);
      out = new BufferedOutputStream(fout);
      out.write(data, offset, size);
      out.flush();
    } catch (FileNotFoundException exc) {
      if (retries <= 0) {
        throw exc;
      }
      _hold(DEFAULT_IO_WAIT);
      writeBytes(target, data, offset, size, --retries);
    } finally {
      if (out != null) {
        try {
          out.close();
        } catch (Exception _) { }
      }
    }
  }

  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // Random access file operations
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////

  public static byte[] readBytes(RandomAccessFile source) throws IOException {
    return readBytes(source, -1L, 0, DEFAULT_READ_BLOCK_SIZE, (short)0);
  }
  
  public static byte[] readBytes(RandomAccessFile source, int maxReadSize) throws IOException {
    int blockSize = _getOptimalBlockSize(maxReadSize);
    return readBytes(source, -1L, maxReadSize, blockSize, (short)0);
  }
  
  public static byte[] readBytes(RandomAccessFile source, int maxReadSize, int blockSize)
                       throws IOException {
    return readBytes(source, -1L, maxReadSize, blockSize, (short)0);
  }
  
  public static byte[] readBytes(RandomAccessFile source, short retries) throws IOException {
    return readBytes(source, -1L, 0, DEFAULT_READ_BLOCK_SIZE, retries);
  }
  
  public static byte[] readBytes(RandomAccessFile source, int maxReadSize, short retries) throws IOException {
    int blockSize = _getOptimalBlockSize(maxReadSize);
    return readBytes(source, -1L, maxReadSize, blockSize, retries);
  }
  
  public static byte[] readBytes(RandomAccessFile source, int maxReadSize, int blockSize, short retries)
                       throws IOException {
    return readBytes(source, -1L, maxReadSize, blockSize, retries);
  }
  
  public static byte[] readBytes(RandomAccessFile source, long position, int maxReadSize) throws IOException {
    int blockSize = _getOptimalBlockSize(maxReadSize);
    return readBytes(source, position, maxReadSize, blockSize, (short)0);
  }
  
  public static byte[] readBytes(RandomAccessFile source, long position, int maxReadSize, int blockSize)
                       throws IOException {
    return readBytes(source, position, maxReadSize, blockSize, (short)0);
  }
  
  public static byte[] readBytes(RandomAccessFile source, long position, int maxReadSize, short retries)
                       throws IOException {
    int blockSize = _getOptimalBlockSize(maxReadSize);
    return readBytes(source, position, maxReadSize, blockSize, (short)0);
  }
  
  public static byte[] readBytes(RandomAccessFile source, long position,
                                 int maxReadSize, int blockSize, short retries)
                       throws IOException {
    byte[] result = new byte[0];
    try {
      if (position >= 0) {
        source.seek(position);
      }
      // HAZARD: operation readFully() cannot be used,
      //         lest a EOFException be thrown -
      //         operations readBytes(InputStream), readBytes(File),
      //         and ReadBytes(RandomAccessFile) must behave identicaly
      byte[] temp;
      int count = (maxReadSize > 0 && maxReadSize < blockSize ? maxReadSize : blockSize);
      byte[] buf = new byte[count];
      while (count >= 0 && (maxReadSize <= 0 || result.length < maxReadSize)) {
        if (maxReadSize <= 0) {
          count = source.read(buf);
        } else {
          count = Math.min(buf.length, maxReadSize - result.length);
          count = source.read(buf, 0, count);
        }
        if (count > 0) {
          temp = new byte[result.length + count];
          if (result.length > 0) {
            System.arraycopy(result, 0, temp, 0, result.length);
          }
          System.arraycopy(buf, 0, temp, result.length, count);
          result = temp;
        }
      }
    } catch (FileNotFoundException exc) {
      if (retries <= 0) {
        throw exc;
      }
      _hold(DEFAULT_IO_WAIT);
      result = readBytes(source, position, maxReadSize, blockSize, --retries); 
    }
    return result;
  }

  public static void writeBytes(RandomAccessFile target, byte[] data) throws IOException {
    writeBytes(target, -1L, data, 0, data.length, (short)0);
  }

  public static void writeBytes(RandomAccessFile target, byte[] data, short retries) throws IOException {
    writeBytes(target, -1L, data, 0, data.length, retries);
  }

  public static void writeBytes(RandomAccessFile target, byte[] data, int offset, int size)
                     throws IOException {
    writeBytes(target, -1L, data, offset, size, (short)0);
  }

  public static void writeBytes(RandomAccessFile target, byte[] data, int offset, int size, short retries)
                     throws IOException {
    writeBytes(target, -1L, data, offset, size, retries);
  }

  public static void writeBytes(RandomAccessFile target, long position, byte[] data) throws IOException {
    writeBytes(target, position, data, 0, data.length, (short)0);
  }

  public static void writeBytes(RandomAccessFile target, long position, byte[] data, short retries)
                     throws IOException {
    writeBytes(target, position, data, 0, data.length, retries);
  }

  public static void writeBytes(RandomAccessFile target, long position, byte[] data, int offset, int size)
                     throws IOException {
    writeBytes(target, position, data, offset, size, (short)0);
  }

  public static void writeBytes(RandomAccessFile target, long position,
                                byte[] data, int offset, int size, short retries)
                     throws IOException {
    try {
      if (position >= 0) {
        target.seek(position);
      }
      target.write(data, offset, size);
    } catch (FileNotFoundException exc) {
      if (retries <= 0) {
        throw exc;
      }
      _hold(DEFAULT_IO_WAIT);
      writeBytes(target, position, data, offset, size, --retries); 
    }
  }

  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // Input/Output stream operations
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////

  public static InputStream createInputStream(String path) throws IOException {
    return createInputStream(path, IoTools.class);
  }

  public static InputStream createInputStream(String path, Class<?> refClass) throws IOException {
    if (refClass == null) {
      throw new IllegalArgumentException("Reference class must be specified");
    }
    InputStream result = (path.startsWith("file:") ? null : refClass.getResourceAsStream(path));
    // class loader might not have been able to create the resource
    if (result == null) {
      File file = createFile(path, refClass);
      result = new FileInputStream(file);
    }
    return result;
  }

  public static OutputStream createOutputStream(String path) throws IOException {
    return createOutputStream(path, IoTools.class);
  }

  public static OutputStream createOutputStream(String path, Class<?> refClass) throws IOException {
    if (refClass == null) {
      throw new IllegalArgumentException("Reference class must be specified");
    }
    URL url = (path.startsWith("file:") ? null : refClass.getResource(path));
    String name = (url == null ? path : url.toExternalForm());
    File file = createFile(name, refClass);
    OutputStream result = new FileOutputStream(file);
    return result;
  }
  
  public static byte[] readBytes(InputStream source) throws IOException {
    return readBytes(source, 0, DEFAULT_READ_BLOCK_SIZE);
  }
  
  public static byte[] readBytes(InputStream source, int maxReadSize) throws IOException {
    int blockSize = _getOptimalBlockSize(maxReadSize);
    return readBytes(source, maxReadSize, blockSize);
  }
  
  public static byte[] readBytes(InputStream source, int maxReadSize, int blockSize) throws IOException {
    byte[] result = new byte[0];
    byte[] temp;
    int count = (maxReadSize > 0 && maxReadSize < blockSize ?  maxReadSize : blockSize);
    byte[] buf = new byte[count];
    while (count >= 0 && (maxReadSize <= 0 || result.length < maxReadSize)) {
      if (maxReadSize <= 0) {
        count = source.read(buf);
      } else {
        count = Math.min(buf.length, maxReadSize - result.length);
        count = source.read(buf, 0, count);
      }
      if (count > 0) {
        temp = new byte[result.length + count];
        System.arraycopy(result, 0, temp, 0, result.length);
        System.arraycopy(buf, 0, temp, result.length, count);
        result = temp;
      }
    }
    return result;
  }
  
  public static int skipBytes(InputStream in, long size) throws IOException {
    int result = 0;
    int count = 0;
    byte[] buf = new byte[Math.min(DEFAULT_READ_BLOCK_SIZE, (int)size)];
    while (count >= 0 && result < size) {
      count = Math.min(DEFAULT_READ_BLOCK_SIZE, (int)(size - result));
      count = in.read(buf, 0, count);
      if (count > 0) {
        result += count;
      }
    }
    return result;
  }

  public static int pipeStreams(OutputStream out, InputStream in) throws IOException {
    return pipeStreams(out, in, 0, DEFAULT_READ_BLOCK_SIZE);
  }

  public static int pipeStreams(OutputStream out, InputStream in, int maxReadSize) throws IOException {
    int blockSize = _getOptimalBlockSize(maxReadSize);
    return pipeStreams(out, in, maxReadSize, blockSize);
  }

  public static int pipeStreams(OutputStream out, InputStream in, int maxReadSize, int blockSize)
                    throws IOException {
    int result = 0;
    int count = 0;
    byte[] buf = new byte[blockSize];
    while (count >= 0 && (maxReadSize == 0 || result < maxReadSize)) {
      if (maxReadSize == 0) {
        count = in.read(buf);
      } else {
        count = Math.min(blockSize, maxReadSize - result);
        count = in.read(buf, 0, count);
      }
      if (count > 0) {
        out.write(buf, 0, count);
        result += count;
      }
    }
    return result;
  }

  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // Private helpers
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  
  private static final int OPTIMAL_READ_BLOCK_FACTOR = 10;
  
  private static int _getOptimalBlockSize(int size) {
    int result;
    if (size > 0) {
      result = Math.min(size / OPTIMAL_READ_BLOCK_FACTOR, MAX_READ_BLOCK_SIZE);
      result = Math.max(result, MIN_READ_BLOCK_SIZE);
      result = Math.min(result, size);
    } else {
      result = DEFAULT_READ_BLOCK_SIZE;
    }
    return result;
  }

  // minimum interval for hold and sleep operations (in milliseconds)
  private static final int MIN_SLEEP_INTERVAL = 10;

  // guaranteed to hold execution of current thread for the specified number of milliseconds
  private static void _hold(int len) {
    int span = 0;
    long start = System.currentTimeMillis();
    while (span < len) {
      try {
        Thread.sleep(Math.max(MIN_SLEEP_INTERVAL, len - span));
      }
      catch (InterruptedException _) { }
      span = (int)(System.currentTimeMillis() - start);
    }
  }

} // class IoTools